import json
import frappe
from frappe.utils import flt, ceil
from erpnext.manufacturing.doctype.production_plan import production_plan
from erpnext.manufacturing.doctype.production_plan.production_plan import (
    ProductionPlan,
    get_exploded_items
)
from erpnext.manufacturing.doctype.bom.bom import get_children
from six import iteritems


class CustomProductionPlan(ProductionPlan):
    @frappe.whitelist()
    def get_sub_assembly_items(self, manufacturing_type=None):
        """ 
        本功能实现 子装配件考虑当前仓库预期可用数量
        仓库逻辑修改为优先取物料主数据中子装配件默认仓库，标准功能继承装配件的（成品)仓码
        取物料主数据中定义的默认供应商
        合并多行子装配件需求数量，以便最后进行最小订单与最小包装量处理
        按料号排序
        预期可用数量按计划行顺序依次扣减
        计划数量按最小订单与最小包装量进行调整
        单位是仅限整数则圆整
        """
        super(CustomProductionPlan,self).get_sub_assembly_items(manufacturing_type = manufacturing_type)

        item_code_map = {}
        if self.sub_assembly_items:
            for row in self.sub_assembly_items:
                item_code = row.production_item
                existing_row = item_code_map.get(item_code)
                if existing_row:
                    existing_row.qty += row.qty
                    #将被合并的数量明细以单独行方式添加到描述字段中，备追溯核对
                    existing_row.description += f'\n{row.parent_item_code}:{row.qty}'
                else:
                    item_code_map[item_code] = row
            self.sub_assembly_items = list(item_code_map.values())
        else:
            return

        #按物料号排序
        self.sub_assembly_items = sorted(self.sub_assembly_items, key=lambda row: row.production_item )
        for i, row in enumerate(self.sub_assembly_items):
            row.idx = i + 1

        item_code_list = [item.production_item for item in self.sub_assembly_items]
        item_code_map = get_item_code_map(item_code_list, self.company)
        for row in self.sub_assembly_items:
            min_order_qty, min_pack_qty, warehouse, supplier = item_code_map.get(row.production_item, (0,0,'',''))
            row.supplier = supplier
            row.fg_warehouse = warehouse or row.fg_warehouse

        item_warehouse_list = tuple({(item.production_item,item.fg_warehouse) for item in self.sub_assembly_items}) 
        projected_qty_map = get_projected_qty_map(item_warehouse_list)

        uom_list = tuple({item.uom for item in self.sub_assembly_items})
        whole_number_uom_list = get_whole_number_uom_list(uom_list)

        for row in self.sub_assembly_items:
            item_code = row.production_item
            min_order_qty, min_pack_qty, warehouse, _ = item_code_map.get(item_code, (0,0,'', ''))
            warehouse, qty = row.fg_warehouse, row.qty
            row.required_qty = qty                          #将原qty（标签为required qty)搬到这个新字段上
            if not self.ignore_existing_ordered_qty:
                key = (item_code, warehouse)
                available_qty = projected_qty_map.get(key, 0)
                row.available_qty = available_qty               #预期可用小于0表示现有需求未满足，会被追加到此次计划中
                reserved_qty = qty if available_qty > qty else available_qty
                row.reserved_qty = reserved_qty * -1
                if key in projected_qty_map:
                    projected_qty_map[key] -= reserved_qty  #调整下一个订单可用库存数量
            else:
                reserved_qty = 0
            plan_qty = row.required_qty - reserved_qty
            row.qty = get_qty(plan_qty, min_order_qty, min_pack_qty)
            if row.uom in whole_number_uom_list:
                row.qty = ceil(row.qty)

    def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders):
        """删除已生成委外采购订单的行"""
        if not subcontracted_po: return
        names = {r.name for r in self.sub_assembly_items if r.type_of_manufacturing == "Subcontract"}
        names = frappe.get_all('Purchase Order',
                                fields = ['`tabPurchase Order Item`.production_plan_sub_assembly_item'],
                                filters={'docstatus': ("<", 2),
                                         "company": self.company,
                                         "production_plan_sub_assembly_item": ("in", names)
                                        },
                                pluck = 'production_plan_sub_assembly_item')
        new_subcontracted_po = {}
        for supplier, po_list in subcontracted_po.items():
            po_list = [r for r in po_list if r.name not in names]
            if po_list: new_subcontracted_po[supplier] = po_list
        return super(CustomProductionPlan, self).make_subcontracted_purchase_order(new_subcontracted_po, purchase_orders)

    def create_work_order(self, item):
        """如果之前已创建工单则不用创建"""
        if item.get("production_plan_item"):
            fieldname = "production_plan_item"
            fieldvalue = item.get("production_plan_item")
        else:
            fieldname = "production_plan_sub_assembly_item"
            fieldvalue = item.get("production_plan_sub_assembly_item")

        if not frappe.db.exists("Work Order", 
            {"company": self.company,
             fieldname: fieldvalue,
             "docstatus": ("<", 2)}):
            return super(CustomProductionPlan, self).create_work_order(item)
        
#由原来的覆盖类方法为覆盖类
production_plan.ProductionPlan = CustomProductionPlan            

original_get_items_for_material_requests = production_plan.get_items_for_material_requests

@frappe.whitelist()
def custom_get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
    """
    标准功能原材料需求由装配件(成品)的最底层物料清单一步计算而来,没有逐级展BOM
    此功能在标准计算基础上将上述子装配件考虑预期可用库存数导致的计划变动也直接按最底层物料清单折算累计的原材料用量，
    在最终计划数量上进行调整(增减)
    受托加工物料不考虑预期可用库存，取物料清单需求数量
    预期可用库存为负数时，将其视为需求量，增加到最终计划数量中
    计划数量按最小订单与最小包装量进行调整
    单位是仅限整数则圆整
    """
    if isinstance(doc, str):
        doc = frappe._dict(json.loads(doc))

    mr_items = original_get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=get_parent_warehouse_data)
    adjust_bom_qty_map = {}         #{物料号:累计调整数量}
    if mr_items:
        #获取子装配件扣减数量折算的底层物料调整数量
        for data in doc.sub_assembly_items:
            planned_qty = flt(data.get('required_qty')) - flt(data.get('qty'))
            if planned_qty:
                item_details = {}
                company = doc.get('company')
                bom_no = data.get('bom_no')
                include_non_stock_items = doc.get('include_non_stock_items')
                item_details = get_exploded_items(item_details,
                            company, bom_no, include_non_stock_items, planned_qty = planned_qty)
                #累计每个子装配件底层物料调整数量                    
                for item_code, details in iteritems(item_details):
                    if item_code in adjust_bom_qty_map:
                        adjust_bom_qty_map[item_code] += flt(details.qty)
                    else:
                        adjust_bom_qty_map[item_code] = flt(details.qty)
        
        item_code_list = tuple({item['item_code'] for item in mr_items})
        item_code_map = get_item_code_map(item_code_list, doc.company)    
        uom_list = tuple({item['uom'] for item in mr_items})
        whole_number_uom_list = get_whole_number_uom_list(uom_list)
        for mr_item in mr_items:
            item_code, quantity = mr_item['item_code'], flt(mr_item['quantity'])
            min_order_qty, min_pack_qty, _, _ = item_code_map.get(item_code, (0,0,'',''))
            mr_item['min_pack_qty'] = min_pack_qty
            required_qty = mr_item["required_bom_qty"]
            if  not doc.ignore_existing_ordered_qty:
                adjust_bom_qty = adjust_bom_qty_map.get(item_code, 0)
                mr_item['adjust_bom_qty'] = adjust_bom_qty * -1
                if doc.include_safety_stock and mr_item["safety_stock"]:
                    required_qty += mr_item["safety_stock"]
                required_qty = 0 if adjust_bom_qty > required_qty else required_qty - adjust_bom_qty
                #预期库存为负(之前需求未满足)，则直接加上该需求       
                required_qty -= mr_item["projected_qty"]                     
            mr_item['quantity'] = get_qty(required_qty, min_order_qty, min_pack_qty)
            if mr_item['uom'] in whole_number_uom_list:
                mr_item['quantity'] = ceil(mr_item['quantity'])                

        mr_items = [mr_item for mr_item in mr_items if mr_item["quantity"]]     #删除数量为0的行      

    return mr_items

production_plan.get_items_for_material_requests = custom_get_items_for_material_requests

def get_item_code_map(item_code_list, company):
    item_list = frappe.get_all("Item", 
            filters = {'name':('in', item_code_list),
                        'company':company},
            fields =['name',
                     'min_order_qty',
                     'min_pack_qty',
                     '`tabItem Default`.default_warehouse',
                     '`tabItem Default`.default_supplier'])
    item_map = {item.name:(item.min_order_qty,item.min_pack_qty,item.default_warehouse, item.default_supplier) 
                for item in item_list}
    return item_map

def get_whole_number_uom_list(uom_list = []):
    return frappe.get_all("UOM",
        filters = {"name": ("in", uom_list),
                    "must_be_whole_number": 1},
        pluck = "name"
        )

def get_projected_qty_map(item_warehouse_list):
    sql = """select 
                    item_code,
                    warehouse,
                    projected_qty
                from 
                    tabBin
                where
                    (item_code,warehouse) in %s
                """               
    projected_qty_list = frappe.db.sql(sql, (item_warehouse_list,), as_dict = True)
    projected_qty_map = {
            (row.item_code,row.warehouse):row.projected_qty
                for row in projected_qty_list
        }
    return projected_qty_map        

def get_qty(qty = 0, min_order_qty = 0, min_pack_qty = 0):
    """ get min pack based qty """
    if qty <=0:
        qty = 0 
    elif qty > (min_order_qty or 0) and min_pack_qty:
        Quotient = qty / min_pack_qty
        rounded_quotient = frappe.utils.floor(Quotient)
        if Quotient > rounded_quotient:
            qty = (rounded_quotient + 1) * min_pack_qty
    elif qty < (min_order_qty or 0):
        qty = min_order_qty
    return qty